using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using JetBrains.Annotations;
using JWT;
using JWT.Exceptions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Silky.Core.Exceptions;
using Silky.Core.Extensions;
using Silky.Http.Core.Configuration;
using Silky.Rpc.Extensions;

namespace Silky.Http.Identity.Authentication.Handlers
{
    internal class SilkyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IOptionsMonitor<GatewayOptions> _gatewayOptionsMonitor;
        private readonly IJwtDecoder _jwtDecoder;
        private static readonly string[] StaticFileExtensions = new[]
        {
            ".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot", ".map"
        };

        public SilkyAuthenticationHandler(
            [NotNull] IOptionsMonitor<AuthenticationSchemeOptions> authenticationSchemeOptions,
            [NotNull] IOptionsMonitor<GatewayOptions> gatewayOptionsMonitor,
            [NotNull] ILoggerFactory logger,
            [NotNull] UrlEncoder encoder,
            [NotNull] ISystemClock clock,
            [NotNull] IJwtDecoder jwtDecoder) :
            base(authenticationSchemeOptions, logger, encoder, clock)
        {
            _jwtDecoder = jwtDecoder ?? throw new ArgumentNullException(nameof(jwtDecoder));
            _gatewayOptionsMonitor = gatewayOptionsMonitor ?? throw new ArgumentNullException(nameof(gatewayOptionsMonitor));
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var path = Context.Request.Path.Value;

            if (IsStaticResource(path))
                return AuthenticateResult.NoResult();

            var serviceEntryDescriptor = Context.GetServiceEntryDescriptor();
            if (serviceEntryDescriptor == null)
            {
                throw new NotFindServiceEntryException(
                    $"Not find service entry by WebAPI {Context.Request.Method}-{Context.Request.Path}");
            }

            var token = GetAuthorizationToken(Context);

            if (serviceEntryDescriptor.IsAllowAnonymous && token.IsNullOrEmpty())
                return AuthenticateResult.NoResult();

            if (token.IsNullOrEmpty())
            {
                return AuthenticateResult.Fail(
                    new AuthenticationException("You have not logged in to the system."));
            }

            var gatewayOptions = _gatewayOptionsMonitor.CurrentValue;
            if (gatewayOptions.JwtSecret.IsNullOrEmpty())
            {
                return AuthenticateResult.Fail(new AuthenticationException(
                    "You have not set JwtSecret on the Gateway configuration node, and the validity of the token cannot be verified"));
            }

            try
            {
                var payload = _jwtDecoder.DecodeToObject(token, gatewayOptions.JwtSecret, verify: true);
                var ticket = CreateTicket(payload);
                return AuthenticateResult.Success(ticket);
            }
            catch (TokenExpiredException)
            {
                return HandleJwtException("Token has expired", serviceEntryDescriptor.IsAllowAnonymous);
            }
            catch (SignatureVerificationException)
            {
                return HandleJwtException("Token has invalid signature");
            }
            catch (Exception ex)
            {
                return HandleJwtException($"The token format is illegal, the reason: {ex.Message}");
            }
        }

        private AuthenticateResult HandleJwtException(string message, bool allowAnonymous = false)
        {
            if (allowAnonymous)
                return AuthenticateResult.NoResult();

            Context.Response.SetResultStatusCode(StatusCode.UnAuthentication);
            throw new AuthenticationException(message);
        }

        private string GetAuthorizationToken(HttpContext httpContext, string headerKey = "Authorization", string tokenPrefix = "Bearer ")
        {
            var bearerToken = httpContext.Request.Headers[headerKey].ToString();
            if (string.IsNullOrWhiteSpace(bearerToken)) return default;

            return bearerToken.StartsWith(tokenPrefix, StringComparison.Ordinal) && bearerToken.Length > tokenPrefix.Length
                ? bearerToken.Substring(tokenPrefix.Length).Trim()
                : bearerToken.Trim();
        }

        private AuthenticationTicket CreateTicket(IDictionary<string, object> payload)
        {
            var claimsIdentity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
            foreach (var (key, value) in payload)
            {
                if (value == null) continue;

                if (key.Equals(ClaimTypes.Role, StringComparison.OrdinalIgnoreCase))
                {
                    claimsIdentity.AddClaims(ParseRoles(key, value.ToString()));
                }
                else
                {
                    claimsIdentity.AddClaim(new Claim(key, value.ToString()));
                }
            }

            return new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), JwtBearerDefaults.AuthenticationScheme);
        }

        private IEnumerable<Claim> ParseRoles(string claimType, string rolesStr)
        {
            if (string.IsNullOrWhiteSpace(rolesStr))
                yield break;

            foreach (var role in rolesStr.Split(',', StringSplitOptions.RemoveEmptyEntries))
            {
                yield return new Claim(claimType, role.Trim());
            }
        }

        private bool IsStaticResource(string? path)
        {
            if (string.IsNullOrWhiteSpace(path)) return false;

            var lowerPath = path.ToLowerInvariant();
            return StaticFileExtensions.Any(ext => lowerPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase));
        }
    }
}
